﻿//-----------------------------------------------------------------------
// <copyright file="Integers.cs" company="Rhys Parry">
//    Copyright Rhys Parry 2008
//    This file is released under the Apache 2.0 License
// </copyright>
//-----------------------------------------------------------------------
namespace Smarts.Sequences
{
    using System;
    using System.Collections.Generic;
    using System.Collections;
    using System.Linq;
    using System.Text;

    /// <summary>
    /// Integers sequence class. Used to build a generator of
    /// Integers.
    /// </summary>
    public class Integers : IEnumerable<int>, IEnumerator<int>, ICloneable
    {
        /// <summary>
        /// The default starting point for a series
        /// </summary>
        private static int globalOrigin = 1;

        /// <summary>
        /// The current integer in the series
        /// </summary>
        private int currentInteger;

        /// <summary>
        /// The value to increment by to calculate the next
        /// number in the series
        /// </summary>
        private int incrementBy;

        /// <summary>
        /// The last value in the series
        /// </summary>
        private int endValue;

        /// <summary>
        /// The number of items in the series
        /// </summary>
        private int numItems;

        /// <summary>
        /// The number of items generated so far in the series
        /// </summary>
        private int numGenerated;

        /// <summary>
        /// Tracks whether the series has an end defined
        /// </summary>
        private bool hasEnd;

        /// <summary>
        /// Tracks whether the series has started
        /// </summary>
        private bool started;

        /// <summary>
        /// The first number in the series
        /// </summary>
        private int origin;

        /// <summary>
        /// Constructs the definition for a series of integers
        /// starting at the global origin
        /// </summary>
        private Integers() : this(Integers.globalOrigin)
        {
        }

        /// <summary>
        /// Constructs the definition for a series of integers
        /// starting at <paramref name="startPoint"/>
        /// </summary>
        /// <param name="startPoint">The first integer in the series</param>
        private Integers(int startPoint)
        {
            this.origin = startPoint;
            this.Reset();
            this.incrementBy = 1;
        }

        /// <summary>
        /// Gets or sets the class level starting point of the sequence
        /// </summary>
        public static int Origin
        {
            get
            {
                return Integers.globalOrigin;
            }

            set
            {
                Integers.globalOrigin = value;
            }
        }

        /// <summary>
        /// Gets the current object in the series
        /// </summary>
        object IEnumerator.Current
        {
            get { return this.currentInteger; }
        }

        #region IEnumerator<int> Members

        /// <summary>
        /// Gets the current value of the sequence
        /// </summary>
        public int Current
        {
            get { return this.currentInteger; }
        }

        #endregion

        /// <summary>
        /// Used to set the starting point of the sequence
        /// </summary>
        /// <param name="startPoint">The first number of the sequence</param>
        /// <returns>A sequence with <paramref name="startPoint"/> as the first value</returns>
        public static Integers From(int startPoint)
        {
            Integers ints = new Integers();
            ints.origin = startPoint;
            ints.Reset();
            return ints;
        }

        /// <summary>
        /// Used to set a final upper bound for the sequence
        /// </summary>
        /// <param name="endPoint">The final value in the sequence</param>
        /// <returns>A sequence with <paramref name="endPoint"/> as the last value</returns>
        public static Integers UpTo(int endPoint)
        {
            Integers ints = new Integers();
            ints.currentInteger = Integers.globalOrigin;
            return ints.To(endPoint);
        }

        /// <summary>
        /// Used to specify that an item exists in the overall unbound
        /// sequence. This method exists primarily so it could be overloaded
        /// </summary>
        /// <param name="item">The item to check</param>
        /// <returns>Whether the Item is a valid member (i.e. an Integer in this case)</returns>
        public static bool Contains(int item)
        {
            return true;
        }

        /// <summary>
        /// Used to set a final upper bound for the sequence
        /// </summary>
        /// <param name="endPoint">The final value in the sequence</param>
        /// <returns>A sequence with <paramref name="endPoint"/> as the last value</returns>
        public Integers To(int endPoint)
        {
            this.endValue = endPoint;
            this.hasEnd = true;
            if (this.endValue < this.currentInteger)
            {
                this.incrementBy = Math.Abs(this.incrementBy) * -1;
            }

            return this;
        }

        /// <summary>
        /// Used to determine whether an item exists in a bound sequence (inclusive).
        /// </summary>
        /// <param name="item">The item to check</param>
        /// <returns>
        /// Whether the item is a member of the bound sequence, also considering the increment
        /// value (i.e. would show up in the enumeration)
        /// </returns>
        public bool Has(int item)
        {
            if (this.hasEnd)
            {
                if (this.endValue >= this.origin)
                {
                    if ((item >= this.origin) && (item <= this.endValue))
                    {
                        return ((item - this.origin) % this.incrementBy) == 0;
                    }
                    else
                    {
                        return false;
                    }
                }
                else
                {
                    if ((item <= this.origin) && (item >= this.endValue))
                    {
                        return ((this.origin - item) % Math.Abs(this.incrementBy)) == 0;
                    }
                    else
                    {
                        return false;
                    }
                }
            }
            else
            {
                if (this.incrementBy > 0 && (item >= this.origin))
                {
                    return ((item - this.origin) % this.incrementBy) == 0;
                }
                else if (this.incrementBy < 0 && (item <= this.origin))
                {
                    return ((this.origin - item) % Math.Abs(this.incrementBy)) == 0;
                }
                else
                {
                    return false;
                }
            }
        }

        /// <summary>
        /// Used to specify the value to increment by for each new number
        /// in the sequence. Use a negative number to go down when there is no end to the sequence
        /// </summary>
        /// <param name="incrementBy">The value to increment by</param>
        /// <returns>A sequence that increments by <paramref name="incrementBy"/></returns>
        public Integers By(int incrementBy)
        {
            if (incrementBy == 0)
            {
                throw new ArgumentOutOfRangeException("incrementBy", "Can not increment by zero");
            }

            if (this.hasEnd && this.endValue < this.currentInteger)
            {
                incrementBy = Math.Abs(incrementBy) * -1;
            }

            this.incrementBy = incrementBy;
            return this;
        }

        /// <summary>
        /// Used to return a certain number of items. Can be used with or instead of
        /// an upper bound. The condition that is met first wins. No more than
        /// <paramref name="numberOfIntegers"/> integers will be returned in the sequence.
        /// </summary>
        /// <param name="numberOfIntegers">The maximum number of integers to return in the sequence</param>
        /// <returns>A sequence with a maximum of <paramref name="numberOfIntegers"/> integers</returns>
        public Integers First(int numberOfIntegers)
        {
            this.numItems = numberOfIntegers;
            return this;
        }

        #region IEnumerable<int> Members

        /// <summary>
        /// Gets the enumerator for the sequence
        /// </summary>
        /// <returns>The enumerator for the sequence</returns>
        public IEnumerator<int> GetEnumerator()
        {
            return Clone() as IEnumerator<int>;
        }

        #endregion

        #region IEnumerable Members

        /// <summary>
        /// Gets the IEnumerator for the series
        /// </summary>
        /// <returns>The IEnumerator for the series</returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return Clone() as IEnumerator;
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Does nothing (required to implement <see cref="IDisposable"/>)
        /// </summary>
        public void Dispose()
        {
            return;
        }

        #endregion

        #region IEnumerator Members

        /// <summary>
        /// Moves to the next value in the sequence
        /// </summary>
        /// <returns>Whether or not the next value is within the bounds of the sequence</returns>
        public bool MoveNext()
        {
            this.numGenerated++;
            if (!this.started)
            {
                this.started = true;
                return true;
            }

            this.currentInteger += this.incrementBy;
            if (this.hasEnd)
            {
                if (this.incrementBy > 0)
                {
                    if (this.currentInteger > this.endValue)
                    {
                        return false;
                    }
                }
                else
                {
                    if (this.currentInteger < this.endValue)
                    {
                        return false;
                    }
                }
            }

            if (this.numItems > 0 && this.numGenerated > this.numItems)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Resets the sequence to its starting point
        /// </summary>
        public void Reset()
        {
            this.currentInteger = this.origin;
            this.started = false;
            this.numGenerated = 0;
        }

        #endregion

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        /// A new object that is a copy of this instance.
        /// </returns>
        public object Clone()
        {
            var clone = new Integers
                            {
                                incrementBy = incrementBy,
                                currentInteger = origin,
                                started = false,
                                numGenerated = 0,
                                numItems = numItems,
                                endValue = endValue,
                                hasEnd = hasEnd,
                                origin = origin
                            };
            return clone;
        }
    }
}
